Introduzione
all'assembly
Gennaio 2000 - Cosma Colanicchia
cloud.cc@tiscalinet.it
Ottimizzato per la risoluzione di 1024x768 pixel
l'assembly
la
struttura
le
variabili
le
istruzioni di trasferimento
le
istruzioni aritmetiche
strutture
di controllo
l'input -
output tramite l'INT 21h
la pila o stack
le
procedure assembly
interfacciamento
tra assembly e C
programmare in assembly tutto quello di cui abbiamo bisogno per programmare
in assembly è un editor di testo puro (l'EDIT del dos va benissimo) e un compilatore
assembly (molto usato in campo didattico il Turbo Assembler della Borland, ma credo se ne
trovino molti validi anche freeware). Tutto quello che dobbiamo fare è scrivere il
codice, salvarlo con estensione .ASM, e poi lanciare l'assembler. Questo in genere non
genera direttamente un file eseguibile, ma un file detto programma oggetto con
estensione .OBJ. Per ottenere l'eseguibile vero e proprio bisogna fare il cosidetto linking
con un secondo programma (di solito distribuito insieme al compilatore) detto linker.
Dopo questo passaggio avremo, finalmente, un file .EXE oppure .COM.
Entrambi i file .EXE e .COM sono eseguibili, ma hanno una struttura leggermente
differente. Molto spesso dobbiamo scrivere programmi molto piccoli, tanto che sia il
codice sia i dati entrano tutti in un solo segmento (64kbyte). In questo caso possiamo
generare un file .COM, più compatto di un .EXE, in cui i valori di tutti registri
segmento coincidono. Questo programma occuperà meno spazio in memoria in fase di
esecuzione.
In assembly, più che in ogni altro linguaggio, è
fondamentale pianificare ogni cosa prima di inziare a scrivere il codice, visto che è
molto facile commettere errori, e trovare un bug in un listato ASM spesso non è affatto
semplice. (date un'occhiata all'appendice sui diagrammi
di flusso)
Andiamo a vedere come è fatta la struttura di un listato assembly
Abbiamo già visto l'utilizzo dei segment di memoria. Nel programma assembly dobbiamo specificare al PC che segmenti intendiamo utilizzare per il codice, per i dati e per lo stack (si tratta di inserire dei valori nei registri segmento: Code Segment, Data Segment, Stack Segment e Extra Segment). In pratica:
IMPORTANTE: Il codice riportate negli esempi si riferisce al compilatore Turbo Assembler della Borland. Nel caso si usi un compilatore differente forse saranno necessarie delle piccole modifiche nella struttura del programma.
;nome del programma
;funzioni del programmaDati SEGMENT
Dati ENDSSistema SEGMENT STACK
Sistema ENDSCodice SEGMENT
Codice ENDSEND <etichetta di partenza>
Nel programma qui sopra non facciamo altro che
inizializzare i vari segmenti tramite le parole SEGMENT e ENDS (che sta per
ENDSegment), che rispettivamente aprono e chiudono il segmento. La parola END serve
a chiudere il programma, e anche ad indicare da quale istruzione partira il programma (gli
associamo a tale scopo un'etichetta). Poi dobbiamo inserire, come abbiamo detto, i valori
dei vari segmenti nei registri segmento del processore. Vediamo come cambia il
programma...
;nome del programma
;funzioni del programmaDati SEGMENT
Dati ENDSSistema SEGMENT STACK
Sistema ENDSCodice SEGMENT
ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
inizio: <...>
<...>
Codice ENDSEND inizio
Abbiamo aggiunto la parola chiave ASSUME e
l'etichetta inizio per la prima istruzione del programma, nel segmento Codice.
Un'etichetta non è altro che una specie di segnalibro, a cui possiamo fare riferimento
quando dobbiamo effettuare dei salti nell'esecuzione del codice. Ne abbiama usata una,
'inizio', in corrispondenza della prima istruzione del programma, e l'abbiamo anche
specificata nell'istruzione END.
A questo punto la struttura base del programma è realizzata.
Nel segmento Codice possiamo inserire le istruzioni e nel segmento Dati possiamo
dichiarare le variabili. Ma come?
le variabili andranno a trovare posto nel segmento di memoria puntato dal
registro Data Segment. Basta dichiararle all'interno del segmento che abbiamo associato a
tale registro (nel nostro caso il segmento Dati).
Possiama dichiarare soltanto due tipi di variabili in
assembly: BYTE e WORD. L'unica differenza tra i due è la lunghezza: un byte
per il primo e due per il secondo (con la parola WORD infatti si intendono in genere due
byte). Per la dichiarazione si usano rispettivamente le parole DB e DW (Dichiara
Byte e Dichiara Word). Inseriamo due variabili nel nostro programma:
;nome del programma
;funzioni del programmaDati SEGMENT
Num1 DB 00h
Num2 DW 00h
Dati ENDSSistema SEGMENT STACK
Sistema ENDSCodice SEGMENT
ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
inizio: <...>
<...>
Codice ENDSEND inizio
Abbiamo aggiunto due righe nel segmento Dati. La prima
dichiara la variabile Num1 di tipo byte, e la inizializza al valore 00h (la 'h' sta ad
indicare che è un valore esadecimale. Possiamo specificare ogni costante numerica anche
come decimale e binaria mettendo al posto di 'h' le lettere 'd' o 'b'). La seconda riga è
analoga alla prima, ma Num2 viene dichiarata di tipo word.
Una struttura dati molto utilizzata nella programmazione è
il vettore, cioè una struttura lineare composta da un numero predefinito di byte
disposti in memoria uno dopo l'altro, a cui viene associato un solo identificativo.
Per dichiarare un vettore in assembly esistono vari modi:
Vett1 DB 00h, 00h, 00h... | Dichiara un vettore di byte di nome Vett1, di lughezza pari al numero di valori di inizializzazione specificati |
Vett2 DW 4 DUP 00h | Dichiara un vettore di word di nome Vett2, di lunghezza 4 e tutti i valori inizializzati a 00h |
Vett3 DB 3 DUP (?) | Dichiara un vettore di byte di nome Vett3, di lunghezza 3 senza inizializzare gli elementi |
Per ora non proccupiamoci oltre dei vettori: sappiano che
esistono e come si dichiarano, diventando più esperti imparerete senz'altro anche ad
usarli.
Prima di passare oltre, vediamo la direttiva EQU. Serve
a definire una costante: in pratica associa un valore ad una certa parola. In fase di
compilazione (cioè mentre il compilatore prende il vostro file sorgente e lo trasforma,
se tutto va bene, in uno eseguibile) ogni costante viene semplicemente sostituita dal suo
valore. Il vantaggio di usare delle costanti sta tutto nella leggibilità del programma:
il listato sarà molto più comprensibile ed eventuali modifiche più semplici e veloci.
Op1 EQU 10d | Associa alla stringa Op1 il valore decimale 10 |
istruzioni
di trasferimento sono tra le operazioni
fondamentali per un programma assembly. Servono a trasferire dati ed indirizzi dalla
memoria ai registri e viceversa.
Per trasferire dati a 8 o 16 bit si usa l'istruzione MOV, il cui formato è:
MOV destinazione, sorgente
dove:
sorgentepuò essere una costante o un riferimento (identificatore) ad un registro o
una variabile
destinazione può essere solo un riferimento (identificatore) ad un registro o a
una variabile
inoltre destinazione e sorgente non possono essere entrambi due variabili e devono essere
compatibili in termini di dimensioni
Ad esempio MOV AX, BX copia il contenuto del registro BX in AX.
MOV AX, 0000h mette il valore 00 esadecimale nel registro AX.
MOV AL, 01h mette il valore 01 esadecimale nel byte basso (AL) di AX.
Quando trasferiamo dati dalla memoria, dobbiamo indicare all'assemblatore il numero dei
byte (1 o 2) a cui stiamo facendo rifermento. Possiamo farlo tramite le direttive BYTE
PTR e WORD PTR (byte pointer e word pointer):
MOV BX, WORD PTR num1
MOV BH, BYTE PTR num2
Per risalire all'indirizzo di una variabile in memoria, usiamo l'istruzione LEA (Load
Effective Address):
LEA destinazione, sorgente
Pone in destinazione (un registro a 16 bit) l'indirizzo di riferimento della variabile
sorgente: LEA AX, num1
istruzioni
logiche servono ad eseguire operazioni
di tipo logico (not, and...) sui dati.
OR destinazione, sorgente pone in destinazione il risultato del OR inclusivo tra
destinazione e sorgente
AND destinazione, sorgente pone in destinazione il risultato dell'AND logico tra
destinazione e sorgente
XOR destinazione, sorgente pone in destinazione il risultato dell'OR esclusivo tra
destinazione e sorgente
NOT destinazione pone in destinazione il risultato del NOT logico di destinazione
dove:
destinazione può essere un registro o una variabile
sorgente può essere un registro, una variabile o una costante
destinazione e sorgente devono essere compatibili in termini di dimensioni
tabella di verità del OR
A | B | OR |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
tabella di verità dell'AND
A | B | AND |
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
tabella di verità dello XOR
A | B | XOR |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
tabella di verità del NOT
A | NOT |
0 | 1 |
1 | 0 |
Esempio:
MOV AL, 11010111b ; mette un valore binario in AL
AND AL, 11111110b ; mette in AL il risultato dell'AND logico tra i bit di AL e 11111110
Nell'esempio, l'istruzione AND di questo tipo mette a 0 l'ultimo bit di AL e lascia invariati gli altri.
istruzioni aritmetiche permettono di effettuare le operazioni aritmetiche di base:
addizione, sottrazione, moltiplicazione e divisione.
ADD destinazione, sorgente effettua la somma tra destinazione e sorgente e mette il
risultato in destinazione
SUB destinazione, sorgente effettua la sottrazione tra destinazione e sorgente e
mette il risultato in destinazione
dove sorgente può essere una costante o il riferimento ad una variabile o ad un registro,
e destinazione può essere solo il riferimento ad una variabile o ad un registro.
MUL sorgente (moltiplicazione tra numeri interi) si comporta in due casi in base
alla lunghezza di sorgente
se sorgente è di tipo BYTE: In AX troviamo il risultato della moltiplicazione tra
sorgente e AL
se sorgente è di tipo WORD: In DX:AX troviamo il risultato della moltiplicazione
tra sorgente e AX
Vediamo che se moltiplichiamo tra loro due valori a 8 bit, il risultato è calcolato su 16
bit, mentre se moltiplichiamo valori a 16 bit il risultato è calcolato su 32 bit (servono
due registri per contenerlo).
IMUL sorgente opera in modo del tutto analogo a MUL, ma considera il bit più
significativo degli operandi come il segno (per informazioni sul sistema binario in
complemento a due consultare l'appendice sistemi di
numerazione).
DIV divisore (divisione tra numeri interi) come MUL, si comporta differentemente in
base alla lunghezza di sorgente
se divisore è di tipo BYTE: viene effettuata la divisione di AX per divisore, il
quoziente viene posto in AL e il resto in AH.
se divisore è di tipo WORD: viene effettuata la divisione di DX:AX per divisore,
il quozieente viene posto in AX e il resto in DX.
IDIV divisore opera come DIV, ma su numeri relativi (come IMUL).
sorgente e divisore devono essere riferimenti ad un registro o ad una variabile.
INC sorgente incrementa di 1 il valore contenuto in sorgente
DEC sorgente decrementa di 1 il valore contenuto in sorgente
dove sorgente deve essere il riferimento ad un registro o ad una variabile.
strutture di
controllo
Il processore esegue le istruzioni così come si presentano,
una dopo l'altra. Tuttavia possiamo, attraverso particolari strutture, controllare il
flusso esecutivo in base ad una determinata condizione. In questo modo possiamo creare
strutture di semplice selezione o di tipo iterativo (cicli). Le istruzioni assembly che
vengono utilizzate per questo scopo sono principalmente di due tipo: salto e confronto.
I salti possono essere incondizionati o condizionati.
JMP indirizzo di riferimento effettua un salto
incondizionato. In genere indirizzo di riferimento è un'etichetta.
Esisitono diverse istruzioni per i salti condizionati:
istruzione | descrizione |
JA | salta se CF=0 e ZF=0 |
JAE | salta se CF=0 |
JB | salta se CF=1 |
JBE | salta se CF=1 e ZF=0 |
JC | salta se CF=1 |
JCXZ | salta se CX=0 |
JE | salta se ZF=1 |
JG | salta se ZF=0 e SF=OF |
JGE | salta se SF=0 |
JL | salta se SF!=OF |
JLE | salta se ZF=1 o SF!=OF |
JNA | salta se CF=1 o ZF=0 |
JNAE | salta se CF=1 |
JNB | salta se CF=0 |
JNBE | salta se CF=0 e ZF=0 |
JNC | salta se CF=0 |
JNE | salta se ZF=0 |
JNG | salta se ZF=1o SF!=OF |
JNGE | salta se SF!=OF |
JNL | salta se SF=OF |
JNLE | salta se ZF=0 e SF=0 |
JNO | salta se OF=0 |
JNP | salta se PF=0 |
JNS | salta se SF=0 |
JNZ | salta se ZF=0 |
JO | salta se OF=1 |
JP | salta se PF=1 |
JPE | salta se PF=1 |
JPO | salta se PF=0 |
JS | salta se SF=1 |
JZ | salta se ZF=1 |
il simbolo "!=" ha valore "diverso da"
La condizione del salto è sempre dettata dai valori del
registro dei flag. I flag più usati per i salti sono:
ZF (flag zero) indica se l'ultima istruzione ha
generato come risultato 0
SF (flag segno) indica se l'ultima istruzione ha
generato un risultato di segno negativo
OF (flag overflow) indica se l'ultima istruzione ha
generato un overflow (con troncamento del bit più significativo del risultato)
La tabella, scritta in quel modo, è di difficile
utilizzo. Tuttavia effettuare un salto condizionato diventa semplicissimo grazie
all'istruzione CMP (compare = confronta).
CMP op1, op2
La CMP si comporta esattamente come l'istruzione SUB destinazione, sorgente con
la differenza che il risultato va perso e i registri destinazione e sorgente (op1 e op2)
rimangono intatti. Quello che viene cambiato è invece il registro dei flag: il base ai
valori di alcuni flag si può dedurre se op1 e op2 sono uguali, se è maggiore il primo
oppure il secondo.
Così diventa semplice effettuare un salto:
CMP AX,0000h ; confronta il valore di AX con il valore 0 esadecimale
JE <etichetta> ; salta se i valori sono uguali
Al posto di 0000h possiamo mettere un qualsiasi valore, ovviamente, in base alle nostre necessità. E anche la condizione può essere diversa:
CMP AL,0Ah ; confronta il valore di AL con il valore 0A esadecimale (10 decimale)
JA <etichetta> ; salta se il primo è maggiore
I nomi dei salti non sono casuali, e ricordarli non è difficile: JE = Jump if Equal (salta se uguale), JA e JB sono rispettivamente: Salta se è maggiore l'operando A (il primo) o quello B (il secondo). Essi possono in genere essere combinati: l'istruzione JAE effettua il salto se op1 è maggiore (JA) o uguale (JE) rispetto a op2. Inoltre i nomi dei salti cambiano se si tratta di numeri interi o frazionari, con segno o senza.
Ecco una tabella simile alla precedente, ma molto più leggibile e più facile da consultare. Conviene averla a portata di mano quando si scrive un programma ASM.
istruzione | descrizione | con op1 ed op2 |
JA | salta se op1>op2 | interi assoluti |
JAE | salta se op1=>op2 | interi assoluti |
JB | salta se op1<op2 | interi assoluti |
JBE | salta se op1=<op2 | interi assoluti |
JE | salta se op1=op2 | |
JG | salta se op1>op2 | interi relativi |
JGE | salta se op1=>op2 | interi relativi |
JL | salta se op1<op2 | interi relativi |
JLE | salta se op1=<op2 | interi relativi |
JNA | salta se op1=<op2 | interi assoluti |
JNAE | salta se op1<op2 | interi assoluti |
JNB | salta se op1=>op2 | interi assoluti |
JNBE | salta se op1>op2 | interi assoluti |
JNE | salta se op1!=op2 | |
JNG | salta se op1<=op2 | interi relativi |
JNGE | salta se op1<op2 | interi relativi |
JNL | salta se op1=>op2 | interi relativi |
JNLE | salta se op1>op2 | interi relativi |
JNZ | salta se op1!=op2 | |
JZ | salta se op1=op2 |
come prima, il simbolo "!=" ha valore "diverso da"
I salti condizionati e non possono venire combinati per formare le principali strutture utilizzate nei programmi: selezione e iterazione.
la struttura di selezione permette, in base al verificarsi o no di una condizione, di scegliere tra due blocchi di istruzioni (uno può anche essere vuoto), corrisponde alla if (condizione) { sequenza 1 } else { sequenza 2 } del linguaggio C.
se Condizione allora
sequenza 1
altrimenti
sequenza 2
fine selezione
Per il modo di operare del processore, è più corretta la seguente forma:
se Condizione allora
esegui la sequenza 1
salta la sequenza 2
altrimenti
salta la sequenza 1
esegui la sequenza 2
fine selezione
In assembly:
JCondizione etichetta
... sequenza 2 ...
JMP fine_sel
etichetta:
... sequenza 1 ...
fine_sel:
In pratica, se la condizione espressa dall'istruzione di
salto utilizzata è verificata, il programma salta sequenza2 ed esegue sequenza1,
altrimenti continua con l'esecuzione di sequenza2 e salta sequenza1.
Vediamo un esempio:
Se AX>0 metti in AX il valore 0, altrimenti poni in AX il
valore 0001h
in assembly:
CMP AX,0000h ;confronto tra AX e 0
JA maggiore ;salta se op1>op2
MOV AX,0001h ;mette in AX il valore 1
JMP fine_sel ;salta incondizionatamente all'etichetta fine_sel
maggiore: MOV AX,0000h ;mette in AX il valore 0
fine_sel:
Il ciclo a controllo in coda
L'iterazione è una struttura che permette di ripetere più
volte un istruzione sotto il controllo di una condizione. In C non mi sembra esista una
struttura di questo tipo. Se conoscete il Pascal, questa equivale alla Repeat
<sequenza> Until condizione. In pratica ripete <sequenza> fino a
quando condizione non si verifica (condizione = True).
ripeti
istruzioni
finchè condizione
in assembly, attraverso la logica dei salti, viene rappresentato così:
inizio_ciclo:
istruzioni
JNcondizione inizio_ciclo
esempio:
MOV AX, 0000h
inizio_ciclo:
INC AX
CMP AX, 000Ah ;confronta AX e il valore 0Ah (10d)
JNE inizio_ciclo ;salta all'inizio (e ripete il ciclo) se diverso
Dato che il controllo della condizione viene eseguito alla fine del ciclo, le istruzioni in sequenza vengono eseguite comunque almeno una volta, anche se la condizione era già verificata in partenza. In pratica:
MOV AX, 000Ah
inizio_ciclo:
INC AX
CMP AX, 000Ah
JNE inizio_ciclo
Questo spezzone di codice dovrebbe controllore se AX = 10d, e in caso contrario incrementare AX. In caso favorevole uscire dal ciclo. Vediamo però che AX vale già 10d, tuttavia tale registro viene comunque incrementato (alla fine varrà 000Bh). Inoltre, in questo particolare programma, il ciclo non finirà mai: AX varrà 11, poi 12, poi 13 e non diventerà mai uguale a 10. Sarebbe buona norma, nelle condizioni, evitare di esprimere un'uguaglianza:
MOV AX, 000Ah
inizio_ciclo:
INC AX
CMP AX, 000Ah
JB inizio_ciclo ; salta se minore (invece di salta se non uguale)
In questo modo abbiamo risolto il problema del ciclo infinito. Tuttavia, a causa del fatto che la sequenza viene eseguita almeno una volta, in genere si evita il ciclo a controllo in coda e si utilizza invece quello a controllo in testa.
Il ciclo a controllo in testa
Una struttura iterativa a controllo in testa si può
descrivere, ad alto livello, così:
mentre condizione
istruzioni
fine ciclo
Equivale alla while (condizione) { sequenza } del C.
in assembly:
inizio_ciclo:
JNcondizione fine_ciclo
sequenza
JMP inizio_ciclo
fine_ciclo
esempio:
inizio_ciclo:
CMP AX,0Ah ;confronta AX con 10d
JNE fine_ciclo ;salta se diverso
INC AX ;incrementa AX
JMP inizio_ciclo
fine_ciclo:
La differenza tra questa struttura e quella a controllo in coda sta nel fatto che se la condizione è inizialmente verificata, la sequenza di istruzioni non viene eseguita nemmeno una volta.
Il ciclo a contatore
Il ciclo a contatore ha una struttura di questo tipo:
ripeti per N volte
sequenza
fine ciclo
Possiamo utilizzare un ciclo a contatore se vogliamo
ripetere un blocco di istruzioni per un numero di volte noto a priori.
i cicli in assembly sono in genere a decremento:
CONTATORE = N
ripeti
sequenza
decrementa CONTATORE
finché CONTATORE = 0
Come contatore si usa di solito il registro CX (registro contatore, appunto), perchè esiste un'istruzione che esegue le ultime due istruzioni automaticamente: l'istruzione LOOP: decrementa CX e, se CX = 0, salta all'etichetta specificata.
Grazie all'istruzione LOOP diventa semplice scrivere un ciclo a contatore in assembly:
MOV CX, <N> ; dove n è il numero di ripetizioni da eseguire
inizio_ciclo:
sequenza
LOOP inizio_ciclo
l'input/output tramite l'INT 21h
L'assembly non prevede funzioni di input/output gà pronte.
Il programmatore deve crearsi le proprie routine o appoggiarsi a quelle create da terze
parti. Tra queste ultime troviamo quelle del DOS, accessibili richiamando l'interrupt 21h.
Basta mettere il codice del servizio richiesto in AX ed usare l'istruzione INT 21h.
Tra le funzioni più comuni per l'input/output da tastiera:
servizio 01h | Aquisizione di un carattere da tastiera con eco sul
video Attende la pressione di un tasto, e restituisce il AL il codice ASCII del tasto premuto |
servizio 07h | Acquisizione di un carattere da tastiera senza eco sul
video Come il servizio 01h, ma non visualizza il carattere sullo schermo |
servizio 02h | visualizzazione di un carattere a video Stampa il carattere il cui codice ASCII è contenuto in DL |
Quindi, per acquisire un carattere (con eco sul video):
MOV AX,0001h ; servizio 01h
INT 21h ; se AX=0001h, allora in AL va il codice ACII del tasto premuto
E volendo poi stamparlo:
MOV DL,AL ; copio il codice ASCII del tasto letto il DL
MOV AX,0002h ; servizio 02h
INT 21h ; se AX=0002h, allora stampa il carattere di codice ASCII in DL
Vediamo che sia le operazioni di acquisizione che di stampa
fanno rifementi ai codici di carattere ASCII. Nel caso si voglia leggere in input una
cifra numerica, per risalire al valore numerico basta sottrarre il valore 40h al suo
codice ASCII. Infatti 40h in ASCII corrisponde al carattere "0", 41h al
"1" e così via...
In assembly, per stampare un solo carattere sfruttando i
servizi DOS, ci vogliono tre righe di programma! Per questo l'interfaccia di un
applicazione si scrive con un linguaggio ad alto livello. Più avanti vedremo come
scrivere programmi utilizzando sia l'assembly sia il C.
la pila o stack Lo stack è un'area di memoria in cui è possibile inserire un elemento con l'istruzione PUSH o estrarne uno con la POP. La particolarità dello stack è che esso è una struttura di tipo LIFO (Last In First Out): in pratica l'istruzione POP estrae l'ultimo elemento inserito tramite la PUSH. Inoltre, ogni elemento della pila è di tipo word. Spieghiamolo con un esempio: una volta eseguite le istruzioni
PUSH 0001h
PUSH 0002h
PUSH 0003h
la pila, inizialmente vuota, conterrà 3 elementi (quindi 3 word = 6 byte). Estraendo i valori:
POP AX ; in AX 0003h
POP BX ; in BX 0002h
POP CX ; in CX 0001h
i valori di AX, BX e CX saranno rispettivamente 0003h, 0002h e
0001h. Questo perchè l'istruzione POP AX ha estratto l'ultimo elemento inserito nella
pila, cioè 0003h.
Abbiamo visto come vengono utilizzati due dei registri puntatori del processore (BP e SP),
servono ad indicare i limiti in memoria entro i quali sono contenuti i dati inseriti nello
stack. Risulta chiaro ora anche il significato della parola stack nella
intestazione del segmento sistema (struttura di un listato assembly),
in pratica indichiamo che quello è il segmento di memoria che dovrà ospitare fisicamente
i dati dello stack.
Lo stack viene utilizzato soprattutto per il salvataggio temporaneo del valore di qualche
registro che serve per altri scopi:
PUSH DX ; salva il registro DX
PUSH AX ; salva il registro AX
MOV DL, 'c' ; mette in DL il codice ASCII del carattere 'c' (con il compilatore Borland TASM, con gli altri non so...)
MOV AX, 0002h ; servizio 02h: stampa di un carattere a video
INT 21h ; stampa il carattere 'c'
POP AX ; ripristina il registro AX
POP DX ; ripristina il registro DX
Inoltre, è utilizzato anche per le chiamate alle procedure.
le procedure
assembly
In una procedure possiamo scrivere il codice per
eseguire operazioni che verranno utilizzate molte volte dal programma. Ad esempio,
possiamo scrivere una procedura che legga un numero a più cifre dalla tastiera.
Per includere un sottoprogramma in un listato di codice assembly, dobbiamo metterlo tra le
parole PROC ed ENDP. Inoltre il tutto va sistemato di
seguito al programma principale. Vediamo come cambia la struttura di un programma con
l'utilizzo di una procedura:
;nome del programma
;funzioni del programmaDati SEGMENT
Dati ENDSSistema SEGMENT STACK
Sistema ENDSCodice SEGMENT
ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
inizio: <...>
<...>
Nome_proc PROC
<...>
<...>
Nome_proc ENDPCodice ENDS
END inizio
Nome_proc è il nome della funzione, ad esempio LeggiNumero.
Per richiamare una procedura si utilizza l'istruzione CALL nome_procedura.
L'istruzione CALL si comporta come l'istruzione JMP, cioè salta alla parte di codice
relativa alla procedura, ma prima di farlo mette nello stack il valore corrente del
registro IP. In questo modo, alla fine della procedura tale valore può essere
ripristinato con l'istruzione RET e l'esecuzione può continuare dal
punto in cui era stata interrotta (per eseguire la procedura). Vediamo un esempio:
;nome del programma
;funzioni del programmaDati SEGMENT
Dati ENDSSistema SEGMENT STACK
Sistema ENDS
Codice SEGMENT
ASSUME CS:Codice, DS:Dati, SS:Sistema,
ES:Dati
inizio: <...>
CALL print_char
<...>
print_char PROC
MOV DL,0040h
MOV AX,0002h
INT 21h
RET
print_char ENDP
Codice ENDS
END inizio
La procedura print_char stampa un carattere (nell'esempio il carattere "0") sullo schermo. Tuttavia, il contenuto del registro AX viene perso. Per ovviare a questo, si salva e successivamente ripristina il suo valore con l'aiuto dello stack:
print_char PROC
PUSH AX ; salva il valore di AX nello stack
MOV DL,0040h
MOV AX,0002h
INT 21h
POP AX ; ripristina il valore di AX
RET
print_char ENDP
Bisogna fare attenzione alle istruzioni che operano sullo stack all'interno di un procedura: nello stack è infatti conservato l'indirizzo di partenza. Dobbiamo sempre effettuare tante PUSH quante POP, altrimenti l'istruzione RET prenderebbe dallo stack un valore diverso da quello cercato e il programma rischia un crash.
Una procedura di questo tipo comunque non ha motivo di essere. Più utile sarebbe una procedura che stampi un carattere qualsiasi, specificato dal programma chiamate. Questo porta un problema: come passare dei parametri alla procedura? Esistono due modi: attraverso i registri o attraverso lo stack. Il modo più semplice è quello di utilizzare dei registri:
;nome del programma
;funzioni del programmaDati SEGMENT
Dati ENDSSistema SEGMENT STACK
Sistema ENDSCodice SEGMENT
ASSUME CS:Codice, DS:Dati, SS:Sistema, ES:Dati
inizio: <...>
MOV DL,0040h
CALL print_char
<...>
print_char PROC
PUSH AX
MOV AX,0002h
INT 21h
POP AX
RET
print_char ENDPCodice ENDS
END inizio
In questo caso il carattere da stampare è stato specificato in un registro dal programma principale. Dato che la funzione di stampa di un carattere da parte dell'Interrupt 21h prende il codice ASCII in DL, è stato utilizzato quel registro per passare il parametro, evitando poi di doverlo spostare all'interno della procedura.
Il metodo più utilizzato però è quello che impega lo stack, che anche se leggermente più complesso lascia maggiore libertà al programmatore. Consiste nel mettere nello stack i parametri da passare prima di richiamare la procedura. Questa poi, come prima operazione, provvederà al loro recupero. La complessità deriva dal fatto che lo stack è utilizzato anche dal sistema per conservare l'indirizzo di ritorno della procedura. La procedura, per accedere ai dati nello stack, potrà utilizzare il puntatore BP (vedi: la cpu).
Per un elenco più completo delle istruzioni assembly, controllate l'appendice.